/**
 * 
 */
package reco.db.conf;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Sets.newHashSet;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Set;

import org.apache.cayenne.access.DataDomain;
import org.apache.cayenne.conf.Configuration;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.map.ObjEntity;

import reco.db.Domain;

/**
 * @author juanjo
 */
public class DefaultRecoDomainLoadStrategy implements RecoDomainLoadStrategy {

    private static final String CONSTRAINTS = "constraints";
    private static final String DEFAULTS = "defaults";

    private final Set<Class<? extends Domain<?>>> domainClasses = newHashSet();

    @Override
    public boolean isDomainClass(Class<?> aClass) {
        if (aClass == null) {
            return false;
        }
        return Domain.class.isAssignableFrom(aClass);
    }

    @Override
    public void loadDomainClass(Class<? extends Domain<?>> domainClass) {
        checkNotNull(domainClass, "domainClass");

        if (isBaseClass(domainClass) || isAlreadyLoaded(domainClass)) {
            return;
        }

        // init super
        loadDomainClass(asDomainClass(domainClass.getSuperclass()));
        doLoadDomainClass(domainClass);
    }

    private void doLoadDomainClass(Class<? extends Domain<?>> domainClass) {
        applyConstraintsOn(domainClass);
        applyDefaultsOn(domainClass);
        markAsLoaded(domainClass);
    }

    private void applyDefaultsOn(Class<? extends Domain<?>> domainClass) {
        if (areDefaultsDefinedOn(domainClass)) {
            defineDefaultsOn(domainClass);
        }
    }

    private void applyConstraintsOn(Class<? extends Domain<?>> domainClass) {
        if (areConstraintsDefinedOn(domainClass)) {
            defineConstraintsOn(domainClass);
        }
    }

    private synchronized boolean markAsLoaded(Class<? extends Domain<?>> domainClass) {
        return this.domainClasses.add(domainClass);
    }

    private synchronized boolean isAlreadyLoaded(Class<? extends Domain<?>> domainClass) {
        return this.domainClasses.contains(domainClass);
    }

    private boolean isBaseClass(Class<?> domainClass) {
        return Domain.class.equals(domainClass);
    }

    @Override
    public void loadCayenneConfiguration(Configuration cayenneConfiguration) {
        checkNotNull(cayenneConfiguration, "cayenneConfiguration");
        for (final DataDomain dataDomain : cayenneConfiguration.getDomains()) {
            loadDataDomainEntities(dataDomain);
        }
    }

    private void loadDataDomainEntities(DataDomain dataDomain) {
        assert dataDomain != null;

        final EntityResolver resolver = dataDomain.getEntityResolver();
        for (final ObjEntity entity : resolver.getObjEntities()) {
            final Class<?> domainClass = entity.getJavaClass();
            if (isDomainClass(domainClass)) {
                loadDomainClass(asDomainClass(domainClass));
            }
        }
    }

    private Class<? extends Domain<?>> asDomainClass(Class<?> aClass) {
        @SuppressWarnings("unchecked")
        final Class<? extends Domain<?>> domainClass = (Class<? extends Domain<?>>) aClass;
        return domainClass;
    }

    private boolean areConstraintsDefinedOn(Class<? extends Domain<?>> domainClass) {
        return hasStaticMethodWithoutArgsDefined(domainClass, CONSTRAINTS);
    }

    private void defineConstraintsOn(Class<? extends Domain<?>> domainClass) {
        executeStaticMethodWithoutArgs(domainClass, CONSTRAINTS);
    }

    private boolean areDefaultsDefinedOn(Class<? extends Domain<?>> domainClass) {
        return hasStaticMethodWithoutArgsDefined(domainClass, DEFAULTS);
    }

    private boolean hasStaticMethodWithoutArgsDefined(Class<? extends Domain<?>> domainClass, String methodName) {
        final Method method;
        try {
            method = domainClass.getDeclaredMethod(methodName);
        } catch (final NoSuchMethodException e) {
            return false;
        }
        return Modifier.isStatic(method.getModifiers());
    }

    private void defineDefaultsOn(Class<? extends Domain<?>> domainClass) {
        executeStaticMethodWithoutArgs(domainClass, DEFAULTS);
    }

    private void executeStaticMethodWithoutArgs(Class<? extends Domain<?>> domainClass, String methodName) {
        final Method method;
        try {
            method = domainClass.getDeclaredMethod(methodName);
        } catch (final NoSuchMethodException e) {
            throw new IllegalStateException(methodName + " is not defined", e);
        }
        method.setAccessible(true);
        try {
            method.invoke(null);
        } catch (final IllegalAccessException e) {
            throw new IllegalStateException(methodName + " is not accessible after setAccessible(true)", e);
        } catch (final InvocationTargetException e) {
            throw new IllegalStateException(methodName + " its failing", e);
        }
    }
}
